<!DOCTYPE HTML>
<html>
<script src='test.js'></script>
<script src='call_function.js'></script>
<script>
function wrap(value) {
  return jsonSerialize(value, []);
}

function unwrap(value, opt_cache) {
  return jsonDeserialize(value, [], opt_cache);
}

var initialCache = getPageCache().cache_;
function clearCache() {
  getPageCache().cache_ = Object.create(initialCache);
}

function isValidUUID(uuid) {
  assertEquals(36, uuid.length);
  var list = uuid.split("-").map(function(x) {return x.length;});
  assertEquals(5, list.length);
  assertEquals(8, list[0]);
  assertEquals(4, list[1]);
  assertEquals(4, list[2]);
  assertEquals(4, list[3]);
  assertEquals(12, list[4]);
  assert(/[a-z0-9-]{36}/.test(uuid));
}

function testUUID() {
  var uuids = {}
  for (var i = 0; i < 100; i++) {
    var uuid = generateUUID();
    isValidUUID(uuid);
    assertEquals(null, uuids[uuid]);
    uuids[uuid] = 1;
  }
}

function testCallFunctionNoArgs(runner) {
  clearCache();

  callFunction(function() { return 1; }, []).then((result) => {
    assertEquals(0, result.status);
    assertEquals(1, result.value);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallFunctionThrows(runner) {
  clearCache();
  let allComplete = 0;
  callFunction(function() { throw new Error('fake error'); },
    []).then((result) => {
      assertEquals(StatusCode.JAVA_SCRIPT_ERROR, result.status);
      assertEquals('fake error', result.value);
      if (allComplete)
        runner.continueTesting();
      allComplete += 1;
    });
  callFunction(function() {
      var e = new Error('fake error');
      e.code = 77;
      e.message = 'CUSTOM';
      throw e;
  }, []).then((result) => {
    assertEquals(77, result.status);
    assertEquals('CUSTOM', result.value);
    if (allComplete)
      runner.continueTesting();
    allComplete += 1;
  });
  runner.waitForAsync();
}

function testCallFunctionArgs(runner) {
  clearCache();

  function func(primitive, elem) {
    return [primitive, elem.querySelector('div')];
  }
  callFunction(func, [1, wrap(document)]).then((result) => {
    assertEquals(0, result.status);
    assertEquals(1, result.value[0]);
    const cache = getPageCache();
    assertEquals(document.querySelector('div'), unwrap(result.value[1], cache));
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallFunctionArgsUnwrappedReturn(runner) {
  clearCache();

  function func(elem) {
    return elem.querySelector('div');
  }
  callFunction(func, [wrap(document)], false, true).then((result) => {
    assertEquals(document.querySelector('div'), result);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCacheWrap() {
  clearCache();

  assertEquals(1, wrap(1));
  assertEquals(1, unwrap(1));
  assertEquals("1", wrap("1"));
  assertEquals("1", unwrap("1"));
  assertEquals(false, wrap(false));
  assertEquals(false, unwrap(false));
  assertEquals(null, wrap(null));
  assertEquals(null, unwrap(null));
  assertEquals(undefined, wrap(undefined));
  assertEquals(undefined, unwrap(undefined));

  var cache = getPageCache();
  var arr = [1, new Array(1, new Object({a: 1, b: {a: 1, b: {}, c: 3}}), 3)];
  var originalJson = JSON.stringify(arr);
  arr[1][1].b.b[ELEMENT_KEY] = cache.idPrefix_ + '-' + cache.nextId_;
  var wrappedJson = JSON.stringify(arr);
  arr[1][1].b.b = document;
  assertEquals(wrappedJson, JSON.stringify(wrap(arr)));
  var unwrapped = unwrap(JSON.parse(wrappedJson), cache);
  assertEquals(document, unwrapped[1][1].b.b);
  unwrapped[1][1].b.b = {};
  assertEquals(originalJson, JSON.stringify(unwrapped));
}

function testObjectWithLengthProperty() {
  clearCache();

  let obj = {length: 200};
  assertEquals(200, wrap({length: 200})['length'])
  assertEquals(JSON.stringify(obj), JSON.stringify(wrap({length: 200})))

  obj = {bar: 'foo', bazz: {length: 150}};
  let wrappedObj = wrap(obj);
  assertEquals('foo', wrappedObj['bar'])
  assertEquals(150, wrappedObj['bazz']['length'])
  assertEquals(JSON.stringify(obj), JSON.stringify(wrappedObj));

  let unwrappedObj = unwrap(obj);
  assertEquals('foo', unwrappedObj['bar'])
  assertEquals(150, unwrappedObj['bazz']['length'])
  assertEquals(JSON.stringify(obj), JSON.stringify(unwrappedObj));
}

function testCacheDoubleWrap() {
  clearCache();

  assertEquals(wrap(document)[ELEMENT_KEY], wrap(document)[ELEMENT_KEY]);
}

function testCacheUnwrapThrows() {
  clearCache();

  try {
    var wrapped = {};
    wrapped[ELEMENT_KEY] = '1';
    unwrap(wrapped, getPageCache());
    assert(false);
  } catch (e) {
  }
}

function testCacheQuerySelector() {
  clearCache();

  var cache = getPageCache();
  assertEquals(document.querySelector('div'),
               unwrap(wrap(document.querySelector('div')), cache));
  assertEquals(document.querySelectorAll('div')[0],
               unwrap(wrap(document.querySelectorAll('div')), cache)[0]);
}

function testCacheStaleRef() {
  clearCache();

  var cache = getPageCache();
  var img = document.createElement('img');
  document.body.appendChild(img);
  var wrappedImg = wrap(img);
  document.body.removeChild(img);
  try {
    unwrap(wrappedImg, cache);
    assert(false);
  } catch (e) {
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, e.code);
  }
}

function testCallFunctionWithShadowHost(runner) {
  clearCache();

  // Set up something in the shadow DOM.
  var host = document.body.appendChild(document.createElement('div'));
  var root = host.attachShadow({ mode: 'open' });
  var shadowDiv = root.appendChild(document.createElement('div'));

  function func(element) {
    return element;
  }
  var wrappedHost = wrap(host);
  callFunction(func, [wrappedHost]).then((result) => {
    assertEquals(0, result.status);
    assertEquals(wrappedHost['ELEMENT'], result.value['ELEMENT']);
    var cache = getPageCache();
    assertEquals(host, unwrap(result.value, cache));
    document.body.removeChild(host);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallFunctionWithShadowRoot(runner) {
  clearCache();

  // Set up something in the shadow DOM.
  var host = document.body.appendChild(document.createElement('div'));
  var root = host.attachShadow({ mode: 'open' });
  var shadowDiv = root.appendChild(document.createElement('div'));

  function func(element) {
    return element;
  }
  var wrappedHost = wrap(host);
  var wrappedRoot = wrap(root);
  
  // Should handle shadow root as an argument.
  callFunction(func, [wrappedRoot]).then((result) => {
    assertEquals(0, result.status);
    assertEquals(wrappedRoot['ELEMENT'], result.value['ELEMENT']);
    var cache = getPageCache();
    assertEquals(root, unwrap(result.value, cache));

    document.body.removeChild(host);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCacheWithShadowDomAttached() {
  clearCache();
  const pageCache = getPageCache();
  
  // Set up something in the shadow DOM.
  var host = document.body.appendChild(document.createElement('div'));
  var root = host.attachShadow({ mode: 'open' });
  var shadowDiv = root.appendChild(document.createElement('div'));
  
  // Test with attached element in shadow DOM.
  var wrappedDiv = wrap(shadowDiv);
  var unwrappedDiv = unwrap(wrappedDiv, pageCache);
  assertEquals(shadowDiv, unwrappedDiv);
  
  document.body.removeChild(host);
}

function testCacheWithShadowDomDetachedChild() {
  clearCache();

  // Set up something in the shadow DOM.
  var host = document.body.appendChild(document.createElement('div'));
  var root = host.attachShadow({ mode: 'open' });
  var shadowDiv = root.appendChild(document.createElement('div'));

  // Test with detached element in shadow DOM.
  var wrappedDiv = wrap(shadowDiv);
  root.removeChild(shadowDiv);
  var rootCache = getPageCache();
  try {
    unwrap(wrappedDiv, rootCache);
    assert(false);
  } catch (e) {
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, e.code);
  }
  
  document.body.removeChild(host);
}

// Verify callFunction works when Object.prototype has user-defined functions.
// (https://crbug.com/chromedriver/3074)
function testCallWithFunctionInObject(runner) {
  clearCache();

  Object.prototype.f = () => {};
  function func(arg) {
    return { bar: arg };
  }
  callFunction(func, [{ foo: 1 }]).then((result) => {
    assertEquals(1, result.value.bar.foo);
    delete Object.prototype.f;
    runner.continueTesting();
  });
  runner.waitForAsync();
}

// Verify array serialization works when Array.prototype.toJSON is defined.
// (https://crbug.com/chromedriver/3084)
function testCallWithArrayToJSON(runner) {
  clearCache();

  Array.prototype.toJSON = () => '["testing"]';
  function func() {
    return [1, 2, 3];
  }
  callFunction(func, []).then((result) => {
    assert(result.value instanceof Array);
    assertEquals(result.value.length, 3);
    assertEquals(result.value[2], 3);
    delete Array.prototype.toJSON;
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallWithModifiedObjectProto(runner) {
  var callCount = 0;
  Object.defineProperty(Object.prototype, 'f', {
    enumerable: true,
    configurable: true,
    get: function() {
      callCount += 1;
    }
  });

  callFunction(function() {}, []).then(() => {
    try {
      assertEquals(callCount, 0);
      runner.continueTesting();
    } finally {
      delete Object.prototype.f;
    }
  });
  runner.waitForAsync();
}
</script>
<body>
<div><span></span></div>
</body>
</html>
